One Close is Always Enough
If a file is closed twice, it is possible to corrupt the file system on
a disk. Without a clear understanding of how the file system allocates
access paths to files that are currently open, it is possible to adopt a
rather cavalier attitude about opening and closing files. This Note
explains why it is necessary to be very careful about opening and
closing files.
When the File Manager receives an Open call, it will look at the
parameters passed in the parameter block and create a new access path
for the file that is being opened. The access path is how the File
Manager keeps track of where to send data that is written, and where to
get data that is read from that file. An access path is nothing more than:
- a buffer that the file system uses to read and write data, and
- a File Control Block that describes how the file is stored on a disk.
A call such as:
ErrStuff = FSpOpenDF (fsspec, permission, firstRefNum);
will create the access path as a buffer and a File Control Block (FCB)
in the FCB buffer. The term "FCB buffer" is used in most documentation,
although it actually behaves more like an array than a buffer. However,
to avoid confusion, this Technote will continue to use the term "FCB
buffer," although "FCB array" would be a better description.
Note:
The following example is here for illustrative purposes only;
dependence on it may cause compatibility problems with future system software.
|
The FCBSPtr is a low-memory global (at 0x034E )
that holds the address of
a nonrelocatable block. That block is the File Control Block buffer, and
is composed of the two byte header which gives the length of the block,
followed by the FCB records themselves. The records are of fixed length,
and give detailed information about an open file. The structure of the
queue can be visualized as:
As depicted, any given record can be found by adding the length of the
previous FCB records to the start of the block, adding 2 for the two
byte header; giving an offset to the record itself. The size of the
block, and hence the number of files that can be open at any given time,
is determined at startup time and expanded on demand later. The call to
open the file referenced by fsspec above, will produce the file
reference number (which refers to the access path to the file) in
firstRefNum . This is the number that will be used to access that file
from that point on. The File Manager passes back an offset into the FCB
buffer as the reference number (RefNum ). This offset is the number of
bytes past the beginning of the queue to that FCB record in the buffer.
That FCB record will describe the file that was opened. An example of a
number that might get passed back as a RefNum is $1D8 .
That also means
that the FCB record is $1D8 bytes into the FCB block.
A visual example of a record in use, and how the RefNum relates is:
Base is merely the address of the nonrelocatable block that is the FCB
buffer. FCBSPtr points to it. The RefNum (a number like $1D8 )
is added to Base , to give an address in the block.
That address is what the file
system will use to read and write to an open file, which is why you are
required to pass the RefNum to the PBRead and PBWrite calls.
So RefNum is merely an offset into the buffer.
Let's step through a dangerous imaginary sequence and see what happens
to a given record in the FCB buffer. Here's the sequence we will step through:
ErrStuff = FSpOpenDF (fsspec, permission, firstRefNum);
ErrStuff = FSClose ( firstRefNum );
ErrStuff = FSpOpenDF (secondFileSpec, permission, secondRefNum);
ErrStuff = FSClose ( firstRefNum ); {the wrong file gets closed!!!}
{the above line will close 'SecondFile', not 'FirstFile', which is
already closed}
|
Before any operations, the record at $1D8 is not used.
After the call:
ErrStuff = FSpOpenDF (firstFileSpec, permission, firstRefNum);
firstRefNum = $1D8 and the record is in use.
After the call:
ErrStuff = FSClose (firstRefNum);
firstRefNum is still equal to $1D8, but the FCB record is unused.
After the call:
ErrStuff = FSpOpenDF (secondFileSpec, permission, secondRefNum);
SecondRefNum = $1D8, FirstRefNum = $1D8, and the record is reused.
After the call:
ErrStuff = FSClose (firstRefNum);
The firstRefNum = $1D8, secondRefNum = $1D8, and the FCB buffer element
is cleared. This happens even though firstFile was already closed.
Actually, secondFile was closed:
Note:
The second close is using the old RefNum . The second close will still
close a file, and in fact will return noErr as its result. Any
subsequent accesses to the secondRefNum will return an error, since the
file 'secondFile ' was closed. The File Control Blocks are reused, and
since they are just offsets, it is possible to get the same file RefNum
back for two different files. In this case, firstRefNum == secondRefNum
since 'firstFile ' was closed before opening 'secondFile ' and the same
FCB record was reused for 'secondFile '.
|
There are any number of nasty cases that can arise if a file is closed
twice, reusing an old RefNum . A common programming practice is to have
an error handler or cleanup routine that goes through the files that a
program creates and closes them all, even if some may already be closed.
If an FCB element was not reused, the Close will return the expected
fnOpnErr . If the FCB had been reused, then the Close could be closing
the wrong file. This can be very dangerous.
As a particularly nasty example, think of what can happen if a program
were to close a file, then the user inserted an HFS floppy disk. The FCB
could be reused for the Catalog File on that HFS disk. If the program
had a generic error handler that closed all of its files, it could
inadvertently close "its" file again. If it thought "its" file was still
open it would do the close, which could close the Catalog file on the
HFS disk. This is catastrophic for the disk since the file could easily
be closed in an inconsistent state. The result is a bad disk that needs
to be reformatted.
|